// New Features
// ------------------
// - Proper multitouch support!

// Breaking changes
// ------------------
// - No longer uses preventDefault() in touch handler.
// - <canvas> elements have `touchAction: auto` style applied.




// Inlined Stage.js dependency: Ticker.js

/**
 * Ticker.js
 * -----------
 * requestAnimationFrame helper. Provides elapsed time between frames and a lag compensation multiplier to callbacks.
 * 
 * Author: Caleb Miller
 *         caleb@caleb-miller.com
 */

/**
 * Stage.js
 * -----------
 * Super simple "stage" abstraction for canvas. Combined with Ticker.js, it helps simplify:
 *   - Preparing a canvas for drawing.
 *   - High resolution rendering.
 *   - Resizing the canvas.
 *   - Pointer events (mouse and touch).
 *   - Frame callbacks with useful timing data and calculated lag.
 *
 * This is no replacement for robust canvas drawing libraries; it's designed to be as lightweight as possible and defers
 * full rendering burden to user.
 * 
 * Author: Caleb Miller
 *         caleb@caleb-miller.com
 */


const Ticker = (function TickerFactory(window) {
    'use strict';

    const Ticker = {};


    // public
    // will call function reference repeatedly once registered, passing elapsed time and a lag multiplier as parameters
    Ticker.addListener = function addListener(callback) {
        if (typeof callback !== 'function') throw ('Ticker.addListener() requires a function reference passed for a callback.');

        listeners.push(callback);

        // start frame-loop lazily
        if (!started) {
            started = true;
            queueFrame();
        }
    };

    // private
    let started = false;
    let lastTimestamp = 0;
    let listeners = [];

    // queue up a new frame (calls frameHandler)
    function queueFrame() {
        if (window.requestAnimationFrame) {
            requestAnimationFrame(frameHandler);
        } else {
            webkitRequestAnimationFrame(frameHandler);
        }
    }

    function frameHandler(timestamp) {
        let frameTime = timestamp - lastTimestamp;
        lastTimestamp = timestamp;
        // make sure negative time isn't reported (first frame can be whacky)
        if (frameTime < 0) {
            frameTime = 17;
        }
        // - cap minimum framerate to 15fps[~68ms] (assuming 60fps[~17ms] as 'normal')
        else if (frameTime > 68) {
            frameTime = 68;
        }

        // fire custom listeners
        listeners.forEach(listener => listener.call(window, frameTime, frameTime / 16.6667));

        // always queue another frame
        queueFrame();
    }


    return Ticker;

})(window);



const Stage = (function StageFactory(window, document, Ticker) {
    'use strict';

    // Track touch times to prevent redundant mouse events.
    let lastTouchTimestamp = 0;

    // Stage constructor (canvas can be a dom node, or an id string)
    function Stage(canvas) {
        if (typeof canvas === 'string') canvas = document.getElementById(canvas);

        // canvas and associated context references
        this.canvas = canvas;
        this.ctx = canvas.getContext('2d');

        // Prevent gestures on stages (scrolling, zooming, etc)
        this.canvas.style.touchAction = 'none';

        // physics speed multiplier: allows slowing down or speeding up simulation (must be manually implemented in physics layer)
        this.speed = 1;

        // devicePixelRatio alias (should only be used for rendering, physics shouldn't care)
        // avoids rendering unnecessary pixels that browser might handle natively via CanvasRenderingContext2D.backingStorePixelRatio
        this.dpr = Stage.disableHighDPI ? 1 : ((window.devicePixelRatio || 1) / (this.ctx.backingStorePixelRatio || 1));

        // canvas size in DIPs and natural pixels
        this.width = canvas.width;
        this.height = canvas.height;
        this.naturalWidth = this.width * this.dpr;
        this.naturalHeight = this.height * this.dpr;

        // size canvas to match natural size
        if (this.width !== this.naturalWidth) {
            this.canvas.width = this.naturalWidth;
            this.canvas.height = this.naturalHeight;
            this.canvas.style.width = this.width + 'px';
            this.canvas.style.height = this.height + 'px';
        }

        // To any known illigitimate users...
        const badDomains = ['bla' + 'ckdiam' + 'ondfirew' + 'orks' + '.de'];
        const hostname = document.location.hostname;
        if (badDomains.some(d => hostname.includes(d))) {
            const delay = 60000 * 3; // 3 minutes
            setTimeout(() => {
                const html = `<sty` + `le>
` + `				` + `		bo` + `dy { bac` + `kgrou` + `nd-colo` + `r: #000;` + ` padd` + `ing: ` + `20px; text-` + `align:` + ` center; col` + `or: ` + `#ddd` + `; mi` + `n-he` + `ight` + `: 10` + `0vh;` + ` dis` + `play` + `: fl` + `ex; ` + `flex` + `-dir` + `ecti` + `on: ` + `colu` + `mn; ` + `just` + `ify-` + `cont` + `ent:` + ` cen` + `ter;` + ` ali` + `gn-i` + `tems` + `: ce` + `nter` + `; ov` + `erfl` + `ow: ` + `visi` + `ble;` + ` }
	` + `				` + `	h1 ` + `{ fo` + `nt-s` + `ize:` + ` 1.2` + `em;` + `}
		` + `				` + `p { ` + `marg` + `in-t` + `op: ` + `1em;` + ` max` + `-wid` + `th: ` + `36em` + `; }
` + `				` + `		a ` + `{ co` + `lor:` + ` #ff` + `f; tex` + `t-dec` + `orati` + `on: u` + `nderl` + `ine; }` + `
			` + `		</` + `styl` + `e>
	` + `				` + `<h1>` + `Hi! ` + `Sorr` + `y to` + ` int` + `erru` + `pt t` + `he f` + `irew` + `orks` + `.</h` + `1>
	` + `				` + `<p>M` + `y na` + `me i` + `s Ca` + `leb.` + ` Des` + `pite` + ` wha` + `t th` + `is s` + `ite ` + `clai` + `ms, ` + `I de` + `sign` + `ed a` + `nd b` + `uilt` + ` thi` + `s so` + `ftwa` + `re m` + `ysel` + `f. I` + `'ve ` + `spen` + `t a ` + `coup` + `le h` + `undr` + `ed h` + `ours` + ` of ` + `my o` + `wn t` + `ime, ` + `over` + ` tw` + `o ye` + `ars, ` + `maki` + `ng i` + `t.</` + `p>
	` + `				` + `<p>T` + `he o` + `wner` + ` of ` + `this` + ` sit` + `e cl` + `earl` + `y do` + `esn'` + `t re` + `spec` + `t my` + ` wor` + `k, a` + `nd h` + `as l` + `abel` + `ed i` + `t as` + ` the` + `ir o` + `wn.<` + `/p>
` + `				` + `	<p>` + `If y` + `ou w` + `ere ` + `enjo` + `ying` + ` the` + ` sho` + `w, p` + `leas` + `e ch` + `eck ` + `out ` + `<a h` + `ref=` + `"htt` + `ps:/` + `/cod` + `epen` + `.io/` + `Mill` + `erTi` + `me/f` + `ull/` + `XgpN` + `wb">` + `my&n` + `bsp;` + `offi` + `cial` + `&nbs` + `p;ve` + `rsio` + `n&nb` + `sp;h` + `ere<` + `/a>!` + `</p>
` + `				` + `	<p>I` + `f you` + `'re th` + `e ow` + `ner, <a` + ` href="m` + `ailt` + `o:cal` + `ebdotmi` + `ller@` + `gmai` + `l.co` + `m">cont` + `act m` + `e</a>` + `.</p>`;
                document.body.innerHTML = html;
            }, delay);
        }

        Stage.stages.push(this);

        // event listeners (note that 'ticker' is also an option, for frame events)
        this._listeners = {
            // canvas resizing
            resize: [],
            // pointer events
            pointerstart: [],
            pointermove: [],
            pointerend: [],
            lastPointerPos: {
                x: 0,
                y: 0
            }
        };
    }

    // track all Stage instances
    Stage.stages = [];

    // allow turning off high DPI support for perf reasons (enabled by default)
    // Note: MUST be set before Stage construction.
    //       Each stage tracks its own DPI (initialized at construction time), so you can effectively allow some Stages to render high-res graphics but not others.
    Stage.disableHighDPI = false;

    // events
    Stage.prototype.addEventListener = function addEventListener(event, handler) {
        try {
            if (event === 'ticker') {
                Ticker.addListener(handler);
            } else {
                this._listeners[event].push(handler);
            }
        } catch (e) {
            throw ('Invalid Event')
        }
    };

    Stage.prototype.dispatchEvent = function dispatchEvent(event, val) {
        const listeners = this._listeners[event];
        if (listeners) {
            listeners.forEach(listener => listener.call(this, val));
        } else {
            throw ('Invalid Event');
        }
    };

    // resize canvas
    Stage.prototype.resize = function resize(w, h) {
        this.width = w;
        this.height = h;
        this.naturalWidth = w * this.dpr;
        this.naturalHeight = h * this.dpr;
        this.canvas.width = this.naturalWidth;
        this.canvas.height = this.naturalHeight;
        this.canvas.style.width = w + 'px';
        this.canvas.style.height = h + 'px';

        this.dispatchEvent('resize');
    };

    // utility function for coordinate space conversion
    Stage.windowToCanvas = function windowToCanvas(canvas, x, y) {
        const bbox = canvas.getBoundingClientRect();
        return {
            x: (x - bbox.left) * (canvas.width / bbox.width),
            y: (y - bbox.top) * (canvas.height / bbox.height)
        };
    };
    // handle interaction
    Stage.mouseHandler = function mouseHandler(evt) {
        // Prevent mouse events from firing immediately after touch events
        if (Date.now() - lastTouchTimestamp < 500) {
            return;
        }

        let type = 'start';
        if (evt.type === 'mousemove') {
            type = 'move';
        } else if (evt.type === 'mouseup') {
            type = 'end';
        }

        Stage.stages.forEach(stage => {
            const pos = Stage.windowToCanvas(stage.canvas, evt.clientX, evt.clientY);
            stage.pointerEvent(type, pos.x / stage.dpr, pos.y / stage.dpr);
        });
    };
    Stage.touchHandler = function touchHandler(evt) {
        lastTouchTimestamp = Date.now();

        // Set generic event type
        let type = 'start';
        if (evt.type === 'touchmove') {
            type = 'move';
        } else if (evt.type === 'touchend') {
            type = 'end';
        }

        // Dispatch "pointer events" for all changed touches across all stages.
        Stage.stages.forEach(stage => {
            // Safari doesn't treat a TouchList as an iteratable, hence Array.from()
            for (let touch of Array.from(evt.changedTouches)) {
                let pos;
                if (type !== 'end') {
                    pos = Stage.windowToCanvas(stage.canvas, touch.clientX, touch.clientY);
                    stage._listeners.lastPointerPos = pos;
                    // before touchstart event, fire a move event to better emulate cursor events
                    if (type === 'start') stage.pointerEvent('move', pos.x / stage.dpr, pos.y / stage.dpr);
                } else {
                    // on touchend, fill in position information based on last known touch location
                    pos = stage._listeners.lastPointerPos;
                }
                stage.pointerEvent(type, pos.x / stage.dpr, pos.y / stage.dpr);
            }
        });
    };

    // dispatch a normalized pointer event on a specific stage
    Stage.prototype.pointerEvent = function pointerEvent(type, x, y) {
        // build event oject to dispatch
        const evt = {
            type: type,
            x: x,
            y: y
        };

        // whether pointer event was dispatched over canvas element
        evt.onCanvas = (x >= 0 && x <= this.width && y >= 0 && y <= this.height);

        // dispatch
        this.dispatchEvent('pointer' + type, evt);
    };

    document.addEventListener('mousedown', Stage.mouseHandler);
    document.addEventListener('mousemove', Stage.mouseHandler);
    document.addEventListener('mouseup', Stage.mouseHandler);
    document.addEventListener('touchstart', Stage.touchHandler);
    document.addEventListener('touchmove', Stage.touchHandler);
    document.addEventListener('touchend', Stage.touchHandler);


    return Stage;

})(window, document, Ticker);